#!/bin/sh
#  bash fetch_push_repo.sh --skip-ssl --force "--thread=20"  --source-username=mmms --target-username=sss -sp=123 -tp=234  -f=repo.list
# 根据提供清单,fetch源头仓库,push到目的仓库
#
#
#
SKIP_SSL=YES
FORCE_PUSH=YES
FETCH_LFS=YES
THREAD_PROCCES=0
for i in "$@"; do 
  case $i in 
    --help|-h) # 帮助信息,不想写,直接看脚本把
    echo "帮助信息没人维护,麻烦编辑脚本直接看"
    exit 0
    ;;
    -skip-ssl|--skip-ssl) # 忽略git的ssl检查
    # 黑客等不法分子可以篡改或窃取客户端和服务器之间传输的信息和数据，从而影响用户的数据安全!
    SKIP_SSL=YES
    ;;
    --force|-f) # push 使用--force参数
    FORCE_PUSH=YES
    ;;
    --lfs) # 使用lfs, 这个会导致同步时间增加
    FETCH_LFS=YES
    ;;
    --thread=*|-t=*) # 并发数,建议设置为0,根据CPU核心数-2并发,如果CPU核心数-2小于等于0,使用1个并发
    THREAD_PROCCES=`echo $i|sed 's/ //g'|awk -F '=' '{print $2}'`
    ;;
    --source-username=*|-su=*) # 来源仓库账号,如果是开源仓库,可以为空
    SOURCE_USERNAME=`echo $i|sed 's/ //g'|awk -F '=' '{print $2}'`
    ;;
    --source-passwd=*|-sp=*) # 来源仓库账号对应密码,如果提供了账号, 那么必须提供密码
    SOURCE_PASSWD=`echo $i|sed 's/ //g'|awk -F '=' '{print $2}'`
    ;;
    --target-username=*|-tu=*) # 目标仓库账号,通常必须提供
    TARGET_USERNAME=`echo $i|sed 's/ //g'|awk -F '=' '{print $2}'`
    ;;
    --target-passwd=*|-tp=*) # 目标仓库账号对应密码,如果提供了账号, 那么必须提供密码
    TARGET_PASSWD=`echo $i|sed 's/ //g'|awk -F '=' '{print $2}'`
    ;;
    --file=*|-f=*) # 仓库关系清单,一行一对,中间用英文逗号分割
    # 如果每行两节,那就默认mirror仓库(同步全部分支和标签),第一节为来源仓库URL,第二节为目标仓库URL
    # 如果每行三节or四节,那就默认是指定分支/标签同步,第一节为来源仓库URL,第二节为来源refs信息,第三节为目标仓库URL,第四节为目标仓库refs; 其中第四节目标仓库refs可以忽略,如果忽略不填,那就使用来源refs名称
    # 其他节数恕不接待,请另请高明或自行修改脚本......
    # 举例:
    # 例1: 镜像同步两个仓库: https://domain.com/source.git,https://domain.com/target.git
    # 例2: 镜像同步两个仓库,但是要求目标仓库分支默认添加前缀: https://domain.com/source.git,refs/heads/*,https://domain.com/target.git,refs/heads/pre_suffix/*
    # 例3: 没想好
    REPO_LIST=`echo $i|sed 's/ //g'|awk -F '=' '{print $2}'`
    ;;
    --bare-dir=*|-bd=*) # 裸仓存储目录,找个大点的磁盘, 根据你同步的仓库大小来定; 如果为空,默认放到脚本所在目录的code_data下面
    BARE_DIR=`echo $i|sed 's/ //g'|awk -F '=' '{print $2}'`
    ;;
    *)
    echo "不支持的参数:[ $i ],请检查输入"
    exit 1
    ;;
  esac 
done
unix_time=`date '+%s'`
function fetch_push_single_ref(){
    # 没有判断错误, 可能会导致目标仓库内容丢失
    fun_source_url=$1
    fun_source_ref=$2
    fun_target_url=$3
    fun_target_ref=$4
    echo "进来了,${fun_source_url},${fun_source_ref},${fun_target_url},${fun_target_ref}"
    fun_repo_name=`basename ${fun_source_url}|sed 's/.git$//g'`.git
    if [ -d ${BARE_DIR}/${fun_repo_name} ];then
        cd ${BARE_DIR}/${fun_repo_name}
        git log -1 --all >/dev/null 2>&1
        if [ $? -gt 0 ];then
            mv  ${BARE_DIR}/${fun_repo_name}  ${BARE_DIR}/${fun_repo_name}_${unix_time}
            git init --bare ${BARE_DIR}/${fun_repo_name}
        fi
    else
        git init --bare ${BARE_DIR}/${fun_repo_name}
    fi
    cd ${BARE_DIR}/${fun_repo_name}
    # 删除所有分支
    git branch -a|xargs -i git branch -D {}
    # 重新从源头fetch
    git fetch -f ${fun_source_url} ${fun_source_ref}:${fun_source_ref}
    # 更新lfs 内容
    if [ "X${FETCH_LFS}" == "XYES" ];then git lfs fetch --all  ${fun_source_url} ;fi
    # 推送到目标分支
    if [ "X${FORCE_PUSH}" == "XYES" ];then
        git push -f ${fun_target_url} ${fun_source_ref}:${fun_target_ref}
        if [ "X${FETCH_LFS}" == "XYES" ];then git lfs push -f ${fun_target_url} ${fun_source_ref}:${fun_target_ref} ;fi
    else
        git push  ${fun_target_url} ${fun_source_ref}:${fun_target_ref}
        if [ "X${FETCH_LFS}" == "XYES" ];then git lfs push  ${fun_target_url} ${fun_source_ref}:${fun_target_ref} ;fi
    fi
}
SKIP_SSL=YES
FORCE_PUSH=YES
FETCH_LFS=YES
if [ "X${SKIP_SSL}" == "XYES" ];then git config --global http.sslVerify false;fi

# 参数格式检查,统一检查, 最后报错
ERROR_MSG=
EXIT_CODE=0
# 来源仓库清单
if [ "X${REPO_LIST}" == "X" ];then
    ERROR_MSG="""${ERROR_MSG}
未提供仓库清单,请检查入参,应该使用--file=文件绝对/相对路径,或者-f=文件绝对/相对路径"""
    EXIT_CODE=400
elif [ ! -f ${REPO_LIST} ];then
    ERROR_MSG="""${ERROR_MSG}
提供的仓库清单文件不存在,file=${REPO_LIST},请检查输入"""
    EXIT_CODE=400
fi
# 裸仓目录
if [ "X${BARE_DIR}" == "X" ];then
    BARE_DIR=$(cd `dirname $0`; pwd)/code_data
fi
if [ ! -d ${BARE_DIR} ];then
    mkdir -p ${BARE_DIR}
fi
if [ "X${SOURCE_USERNAME}" != "X" -a "X${SOURCE_PASSWD}" == "X" ];then
    ERROR_MSG="提供了SOURCE_USERNAME=${SOURCE_USERNAME},但未提供SOURCE_PASSWD,请检查"
    EXIT_CODE=400
fi
if [ "X${TARGET_USERNAME}" != "X" -a "X${TARGET_PASSWD}" == "X" ];then
    ERROR_MSG="""${ERROR_MSG}
提供了TARGET_USERNAME=${TARGET_USERNAME},但未提供TARGET_PASSWD,请检查"""
    EXIT_CODE=400
fi
if [ ${EXIT_CODE} -gt 0 ];then echo "${ERROR_MSG}"; exit ${EXIT_CODE};fi

# 调试信息打印,临时的,我一定会记得删除的
echo """SKIP_SSL=${SKIP_SSL}
FORCE_PUSH=${FORCE_PUSH}
THREAD_PROCCES=${THREAD_PROCCES}"""

# 以下shell并发引用自https://www.jianshu.com/p/701952ffb755, 搜索到很多, 不知道哪里是源头......
[ -e ./fd1 ] || mkfifo ./fd1 # Step1 创建有名管道
exec 3<> ./fd1   # 创建文件描述符，以可读（<）可写（>）的方式关联管道文件，这时候文件描述符3就有了有名管道文件的所有特性
rm -rf ./fd1      # 关联后的文件描述符拥有管道文件的所有特性,所以这时候管道文件可以删除，我们留下文件描述符来用就可以了              
# Step2 创建令牌

for((i=1;i<=${THREAD_PROCCES};i++))                 # &3 引用文件描述符3，循环向管道中放入了10个令牌,支持并发数为10
do
    echo >&3
done
JUST_NUM=0
ALL_LINES=`cat ${REPO_LIST}|wc -l`
# Step3 拿出令牌，进行并发操作
while read ONE_REPO
do
    read -u3                    # read 命令每次读取一行，也就是拿到一个令牌   
    {
        JUST_NUM=$((JUST_NUM+1))
        echo  "${JUST_NUM}/${ALL_LINES},${ONE_REPO}"
        # 获取一行内容的参数个数
        ARGS_NUM=`echo $ONE_REPO|awk -F ',' '{print NF}'`
        # 只支持三个值,2 3 4,具体看--file的注释
        if [ "X${ARGS_NUM}" != "X2" -a "X${ARGS_NUM}" != "X3" -a "X${ARGS_NUM}" != "X4" ];then
            echo "ERROR,[${ONE_REPO}]有$ARGS_NUM 节,但是目前只支持2 3 4节,请检查仓库清单"
            EXIT_CODE=500
        fi
        if [ "X${ARGS_NUM}" == "X2" ];then
            SOURCE_URL_TMP=`echo $ONE_REPO|awk -F ',' '{print $1}'`
            TARGET_URL_TMP=`echo $ONE_REPO|awk -F ',' '{print $2}'`
            SOURCE_REFS='refs/*'
            TARGET_REFS='refs/*'
        elif [ "X${ARGS_NUM}" == "X3" ];then
            SOURCE_URL_TMP=`echo $ONE_REPO|awk -F ',' '{print $1}'`
            SOURCE_REFS=`echo $ONE_REPO|awk -F ',' '{print $2}'`
            TARGET_URL_TMP=`echo $ONE_REPO|awk -F ',' '{print $3}'`
            TARGET_REFS=${SOURCE_REFS}
        elif [ "X${ARGS_NUM}" == "X4" ];then
            SOURCE_URL_TMP=`echo $ONE_REPO|awk -F ',' '{print $1}'`
            SOURCE_REFS=`echo $ONE_REPO|awk -F ',' '{print $2}'`
            TARGET_URL_TMP=`echo $ONE_REPO|awk -F ',' '{print $3}'`
            TARGET_REFS=`echo $ONE_REPO|awk -F ',' '{print $4}'`
        fi
        # 判断来源于目标账号,如果存在,将URL替换为带账号密码的URL
        if [ "X${SOURCE_USERNAME}" != "X" ];then
            SOURCE_URL_PASSWD=`echo ${SOURCE_PASSWD}|tr -d '\n'|od -An -tx1|tr ' ' %|tr -d '\n'`
            SOURCE_URL=`echo ${SOURCE_URL_TMP}|sed "s#http://#http://${SOURCE_USERNAME}:${SOURCE_URL_PASSWD}@#g;s#https://#https://${SOURCE_USERNAME}:${SOURCE_URL_PASSWD}@#g;"`
        else
            SOURCE_URL=${SOURCE_URL_TMP}
        fi
        if [ "X${TARGET_USERNAME}" != "X" ];then
            TARGET_URL_PASSWD=`echo ${TARGET_PASSWD}|tr -d '\n'|od -An -tx1|tr ' ' %|tr -d '\n'`
            TARGET_URL=`echo ${TARGET_URL_TMP}|sed "s#http://#http://${TARGET_USERNAME}:${TARGET_URL_PASSWD}@#g;s#https://#https://${TARGET_USERNAME}:${TARGET_URL_PASSWD}@#g;"`
        else
            TARGET_URL=${TARGET_URL_TMP}
        fi
        fetch_push_single_ref "${SOURCE_URL}" "${SOURCE_REFS}" "${TARGET_URL}" "${TARGET_REFS}"
        echo >&3                # 执行完一条命令会将令牌放回管道

    }&
done<${REPO_LIST}
wait
exec 3<&-                       # 关闭文件描述符的读
exec 3>&-                       # 关闭文件描述符的写